Skip to content

Sponsor

Sponsor

The Update Object Explained

The update object is the JavaScript object you pass to .update(). It's the heart of the entire method — understanding its structure unlocks everything.


Quick Start (30 Seconds)

javascript
// The update object is just a plain JavaScript object
element.update({
  textContent: 'Hello World',       // Set text
  disabled: false,                  // Set a property
  style: { color: 'red' },          // Apply styles
  classList: { add: 'active' },     // Manage classes
  setAttribute: { 'aria-label': 'Button' }, // Set attributes
  dataset: { userId: '42' },        // Set data attributes
  focus: []                         // Call a DOM method
});

That one object — with those 7 keys — does what would normally take 7+ separate lines of vanilla JavaScript.


What Is the Update Object?

Simply put, the update object is a JavaScript plain object ({}) where:

  • Each key tells .update() what to change on the element
  • Each value tells .update() what to set it to

Think of it as a recipe card you hand to .update(). The recipe says: "Set these properties, apply these styles, toggle these classes, fire these methods." .update() reads the recipe and executes every instruction in the right order.


Syntax

javascript
// Basic form
element.update({
  property: value,
  anotherProperty: anotherValue
});

// With all possible key types
element.update({
  // === Text properties ===
  textContent: 'Plain text',
  innerHTML: '<strong>HTML</strong>',
  innerText: 'Rendered text',

  // === Basic DOM properties ===
  value: 'input value',
  disabled: true,
  checked: false,
  placeholder: 'Enter text...',
  href: 'https://example.com',

  // === Nested object handlers ===
  style: { color: 'red', fontSize: '16px' },
  classList: { add: 'active', remove: 'hidden', toggle: 'open' },
  setAttribute: { 'aria-label': 'Close', 'role': 'button' },
  dataset: { userId: '42', section: 'header' },

  // === Method calls (array = arguments) ===
  focus: [],         // element.focus()
  click: [],         // element.click()
  blur: [],          // element.blur()
  scrollIntoView: [{ behavior: 'smooth' }]  // element.scrollIntoView(...)
});

Why Does This Exist?

When Vanilla JavaScript Is Your Starting Point

In plain JavaScript, you update an element like this:

javascript
const button = document.getElementById('submitBtn');

button.textContent = 'Submitting...';
button.disabled = true;
button.style.opacity = '0.5';
button.style.cursor = 'not-allowed';
button.classList.add('loading');
button.classList.remove('idle');
button.setAttribute('aria-busy', 'true');
button.dataset.state = 'loading';

That's 8 separate statements for one conceptual action: "put the button in loading state."

This approach is perfectly valid. It's straightforward and familiar. But it has a structural limitation: the instructions are scattered. There's no single place that says "here's what the loading state looks like."

When a Unified Description Is Your Goal

The update object solves this by letting you describe the entire state change in one place:

javascript
button.update({
  textContent: 'Submitting...',
  disabled: true,
  style: { opacity: '0.5', cursor: 'not-allowed' },
  classList: { add: 'loading', remove: 'idle' },
  setAttribute: { 'aria-busy': 'true' },
  dataset: { state: 'loading' }
});

This approach is especially useful when: ✅ You want to see the complete state change at a glance ✅ You need to reuse the same configuration multiple times ✅ You want to store state descriptions as variables or factory functions ✅ You're building state-driven UIs where UI = f(state) ✅ You want change detection to skip unnecessary DOM writes automatically

The Choice Is Yours:

  • Use vanilla DOM when making a single, targeted change
  • Use the update object when applying multiple related changes as one conceptual unit
  • Both approaches are valid and can coexist in the same codebase

Mental Model: The Recipe Card

Imagine you're a chef (the browser), and .update() is your sous-chef. You don't bark individual orders at your sous-chef ("Now add salt! Now stir! Now reduce heat!"). Instead, you hand them a recipe card with all the instructions laid out:

Recipe Card: "Loading State"
─────────────────────────────
Text:      "Submitting..."
Disabled:  true
Opacity:   0.5
Classes:   +loading, -idle
ARIA:      aria-busy = true

The sous-chef reads the card top to bottom and executes everything. You described the destination, not the steps. That's the update object.

You Write:                     .update() Executes:
─────────────────────          ──────────────────────────
{                              element.textContent = ...
  textContent: '...',    →     element.disabled = true
  disabled: true,        →     element.style.opacity = ...
  style: {...},          →     element.classList.add(...)
  classList: {...}            element.classList.remove(...)
}                              (change detection skips if same)

How Does It Work?

When you call element.update(updateObject), here's what happens internally:

element.update({ textContent: 'Hi', style: { color: 'red' } })


┌──────────────────────────────────────────┐
│  Read update object keys one by one      │
└──────────────────────────────────────────┘

   ├─→ Key: "textContent"
   │      Type: string primitive
   │      Handler: Direct property assignment
   │      Change detection: lastValue === 'Hi'? → skip
   │      Result: element.textContent = 'Hi'

   ├─→ Key: "style"
   │      Type: object (special handler)
   │      Handler: Style dispatcher
   │      Loops through nested keys:
   │        "color" → element.style.color = 'red'
   │      Change detection applied per style property

   └─→ Returns: element (for chaining)

Key Dispatch Logic

The dispatcher identifies each key's type and routes it:

Key in update object

        ├─→ "style"       → style handler (applies each CSS property)
        ├─→ "classList"   → classList handler (add/remove/toggle/replace)
        ├─→ "setAttribute"→ setAttribute handler (loops key-value pairs)
        ├─→ "dataset"     → dataset handler (camelCase → data-kebab)
        ├─→ "addEventListener" / "removeEventListener" → event handler
        ├─→ value is Array → method call (element[key](...args))
        └─→ anything else  → direct property assignment

Each handler applies change detection before writing to the DOM. If the value is the same as last time, it's skipped entirely.


Basic Usage

Step 1: The Simplest Update Object

Start with one key:

javascript
element.update({ textContent: 'Hello' });
// Same as: element.textContent = 'Hello'
// But: skips the write if text hasn't changed

Step 2: Multiple Properties

Add more keys:

javascript
element.update({
  textContent: 'Hello',
  disabled: false
});
// Same as:
// element.textContent = 'Hello'
// element.disabled = false

Step 3: Nested Object Handlers

Some keys take objects as values:

javascript
element.update({
  style: {
    backgroundColor: '#3b82f6',  // Always camelCase
    color: 'white',
    padding: '10px 20px'
  }
});

Step 4: Special Handlers

javascript
element.update({
  classList: {
    add: ['active', 'visible'],  // Add multiple
    remove: 'hidden',            // Remove one
    toggle: 'open'               // Toggle
  },
  setAttribute: {
    'aria-expanded': 'true',
    'role': 'button'
  },
  dataset: {
    userId: '42',       // Sets data-user-id="42"
    section: 'header'   // Sets data-section="header"
  }
});

Step 5: Method Calls

Use an array as the value to call a DOM method:

javascript
element.update({
  focus: [],                          // element.focus()
  scrollIntoView: [{ behavior: 'smooth' }]  // element.scrollIntoView(...)
});

Deep Dive: The 4 Value Types

Type 1: Primitive Values (String, Number, Boolean)

These are applied as direct property assignments:

javascript
element.update({
  textContent: 'Hello',     // string
  disabled: true,           // boolean
  tabIndex: 0,              // number
  value: 'user@email.com',  // string
  checked: false            // boolean
});

// Internally:
// element.textContent = 'Hello'
// element.disabled = true
// element.tabIndex = 0
// etc.

Change detection: Uses strict equality (===). If disabled was already true, the DOM write is skipped.


Type 2: Nested Objects (Special Handlers)

Four keys are recognized as special handlers that take objects:

javascript
// style — CSS properties in camelCase
element.update({
  style: {
    backgroundColor: 'blue',
    fontSize: '16px',
    marginTop: '10px'
  }
});

// classList — class management methods
element.update({
  classList: {
    add: 'active',              // or ['active', 'visible']
    remove: 'hidden',           // or ['hidden', 'inactive']
    toggle: 'open',             // or { force: true }
    replace: ['old', 'new']     // replaceClass(old, new)
  }
});

// setAttribute — HTML attributes
element.update({
  setAttribute: {
    'aria-label': 'Close dialog',
    'data-id': '42',
    'role': 'button'
  }
});

// dataset — data attributes (camelCase auto-converted)
element.update({
  dataset: {
    userId: '42',       // → data-user-id="42"
    postType: 'article' // → data-post-type="article"
  }
});

Type 3: Arrays (Method Calls)

If the value of a key is an array, .update() treats it as a method call. The array items become the arguments:

javascript
element.update({
  focus: [],                              // element.focus()
  blur: [],                               // element.blur()
  click: [],                              // element.click()
  scrollIntoView: [],                     // element.scrollIntoView()
  scrollIntoView: [{ behavior: 'smooth' }],  // element.scrollIntoView({behavior:'smooth'})
  setAttribute: ['aria-hidden', 'true'],  // element.setAttribute('aria-hidden', 'true')
  removeAttribute: ['aria-hidden'],       // element.removeAttribute('aria-hidden')
  dispatchEvent: [new Event('change')]    // element.dispatchEvent(event)
});

Key Rule: Always use [] for method calls, never just the method name alone:

javascript
// ❌ WRONG — this is a shorthand property, not a method call
element.update({ focus });   // SyntaxError or wrong behavior

// ✅ CORRECT — empty array signals: call this method with no arguments
element.update({ focus: [] });

Type 4: Functions (Dynamic Values)

You can pass a function as a value — it will be called with the element and the result used as the value:

javascript
element.update({
  // Function receives the element, returns the value to set
  textContent: (el) => `Count: ${el.dataset.count}`
});

This is useful when the new value depends on the element's current state.


Deep Dive: Dynamic Update Objects

The update object doesn't have to be hardcoded. Build it dynamically:

Conditional Keys

javascript
function getButtonUpdate(isLoading) {
  return {
    textContent: isLoading ? 'Loading...' : 'Submit',
    disabled: isLoading,
    style: {
      opacity: isLoading ? '0.6' : '1',
      cursor: isLoading ? 'not-allowed' : 'pointer'
    },
    // Conditionally include a key
    ...(isLoading && { classList: { add: 'loading', remove: 'idle' } }),
    ...(!isLoading && { classList: { add: 'idle', remove: 'loading' } })
  };
}

button.update(getButtonUpdate(true));
button.update(getButtonUpdate(false));

Stored as Variables

javascript
const loadingState = {
  textContent: 'Loading...',
  disabled: true,
  style: { opacity: '0.6' }
};

const readyState = {
  textContent: 'Submit',
  disabled: false,
  style: { opacity: '1' }
};

// Apply like swapping a state
button.update(loadingState);
// ... later ...
button.update(readyState);

Built in Factory Functions

javascript
const States = {
  loading: () => ({
    textContent: 'Loading...',
    disabled: true,
    classList: { add: 'loading' }
  }),

  error: (message) => ({
    textContent: message,
    style: { color: '#dc2626', display: 'block' }
  }),

  success: (message) => ({
    textContent: message,
    style: { color: '#16a34a', display: 'block' }
  })
};

button.update(States.loading());
errorDiv.update(States.error('Invalid email'));
successDiv.update(States.success('Saved!'));

Deep Dive: What Makes a Key "Special"?

Most keys in the update object map directly to a DOM property:

javascript
{ textContent: 'Hi' }  →  element.textContent = 'Hi'
{ disabled: true }     →  element.disabled = true
{ value: 'abc' }       →  element.value = 'abc'

But four keys receive special treatment because they need to be handled differently:

KeyWhy It's SpecialHow It's Handled
styleCSS properties need to go on element.style, not the element itselfLoops through nested object, sets each element.style[prop]
classListClasses have add/remove/toggle operations, not a simple valueDispatches to classList.add(), .remove(), .toggle(), .replace()
setAttributeAttributes require element.setAttribute(name, value) APILoops through nested object, calls setAttribute per entry
datasetData attributes need camelCase → kebab-case conversionConverts key casing, then sets element.dataset[key]

And two more for events:

KeyWhy It's SpecialHow It's Handled
addEventListenerEvents need handler reference trackingRegisters with element.addEventListener
removeEventListenerEvents need the original reference to removeFinds and removes the matching handler

Deep Dive: Validation and Edge Cases

Empty Update Object

Passing an empty object is valid — it's a no-op:

javascript
element.update({});  // Perfectly fine, does nothing

Non-Object Argument

If you accidentally pass something other than an object:

javascript
element.update('hello');  // Logs a warning, returns element
element.update(null);     // Logs a warning, returns element
element.update(42);       // Logs a warning, returns element

The method is forgiving — it won't throw, but it will warn you.

Unknown Keys

If you pass a key that doesn't exist on the element, .update() tries to set it as a direct property (which is usually a no-op or sets a custom property):

javascript
element.update({ nonExistentProp: 'value' });
// Tries: element.nonExistentProp = 'value'
// Mostly harmless, but not useful

Null Values

Setting a property to null:

javascript
element.update({ textContent: null });
// Sets: element.textContent = null
// Browser converts null → "" for textContent

Real-World Example: Form State Machine

Here's an update object pattern for managing a form through multiple states:

javascript
const FormStates = {
  idle: {
    submitBtn: {
      textContent: 'Submit',
      disabled: false,
      style: { opacity: '1', cursor: 'pointer' },
      classList: { add: 'btn-primary', remove: 'btn-loading', remove: 'btn-success', remove: 'btn-error' }
    },
    statusMsg: {
      style: { display: 'none' },
      textContent: ''
    }
  },

  submitting: {
    submitBtn: {
      textContent: 'Submitting...',
      disabled: true,
      style: { opacity: '0.6', cursor: 'not-allowed' },
      classList: { add: 'btn-loading', remove: 'btn-primary' }
    },
    statusMsg: {
      style: { display: 'block', color: '#6b7280' },
      textContent: 'Please wait...'
    }
  },

  success: {
    submitBtn: {
      textContent: 'Submitted!',
      disabled: true,
      style: { opacity: '1', cursor: 'default' },
      classList: { add: 'btn-success', remove: 'btn-loading' }
    },
    statusMsg: {
      style: { display: 'block', color: '#16a34a' },
      textContent: 'Form submitted successfully!'
    }
  },

  error: (message) => ({
    submitBtn: {
      textContent: 'Try Again',
      disabled: false,
      style: { opacity: '1', cursor: 'pointer' },
      classList: { add: 'btn-error', remove: 'btn-loading' }
    },
    statusMsg: {
      style: { display: 'block', color: '#dc2626' },
      textContent: message
    }
  })
};

// Apply states
Elements.update(FormStates.idle);         // Reset form
Elements.update(FormStates.submitting);   // Show loading
Elements.update(FormStates.success);      // Show success
Elements.update(FormStates.error('Network error'));  // Show error

Notice: the update objects describe states, not steps. You can read FormStates.submitting and immediately understand what the UI looks like in that state.


Key Insight: One Object, One Conceptual Change

The most important mental shift with the update object is this:

One update object = one conceptual UI change

❌ Thinking in steps:               ✅ Thinking in states:
────────────────────                ──────────────────────────
button.textContent = 'Loading'      button.update({
button.disabled = true                textContent: 'Loading',
button.style.opacity = '0.6'          disabled: true,
button.classList.add('loading')       style: { opacity: '0.6' },
button.setAttribute(...)              classList: { add: 'loading' },
                                      setAttribute: { 'aria-busy': 'true' }
                                    });

The left side is a sequence of instructions. The right side is a description of the desired state. The left side tells the browser how to change; the right side tells it what you want.


Summary

The update object is the core interface of .update():

  • It's a plain JavaScript object ({})
  • Keys identify what to change: DOM properties, style, classList, attributes, dataset, or methods
  • Values can be primitives, nested objects, arrays (for method calls), or functions
  • Four special handlers (style, classList, setAttribute, dataset) receive objects and dispatch intelligently
  • Array values trigger DOM method calls: focus: []element.focus()
  • Dynamic update objects (built in variables, factories, conditionals) are fully supported
  • Change detection runs on every key — skips DOM writes when nothing has changed
  • The whole object represents one conceptual state change

Master the update object and you master .update().


What's Next?

Next chapter: Understanding Parameters & Returns — what goes in, what comes out, and how to use return values for chaining and error checking.